﻿#region Header

/*
Behavioral Rating of Dancing Human Crowds based on Motion Patterns
By

Pascal Hauser 
Dipl. Ing. in Informatik, Hochschule für Technik Rapperswil, 2006
Master Thesis, Hochschule für Technik Rapperswil, 2008-2010

and

Raphael Gfeller
Dipl. Ing. in Informatik, Hochschule für Technik Rapperswil, 2006
Master Thesis, Hochschule für Technik Rapperswil, 2008-2010

*/

#endregion

#region Usings

using System;
using System.Collections.Generic;
using System.Linq;
using paravili.Services;
using Sebarf.Services.Interfaces;

#endregion

namespace paravili.Steps {
	/// <summary>
	/// tries to find movement between the given list of points
	/// </summary>
	public class MatchNearestClusterFirst : ProcessStepConverterWithMeasurement<IList<IList<ColoredPoint>>, IList<Cluster>> {
		#region Public Properties

		[ConfigurableNumericValue(Name = "MaxDistanceBetweenTwoCluster", RangeFrom = 10, RangeTo = 500)]
		public int MaxDistanceBetweenTwoCluster { get; set; }

		[ConfigurableNumericValue(Name = "MaxFramesBetweenTwoCluster", RangeFrom = 1, RangeTo = 50)]
		public int MaxFramesBetweenTwoCluster { get; set; }

		[ConfigurableNumericValue(Name = "WeightOfDistance", RangeFrom = 1, RangeTo = 1000)]
		public int WeightOfDistance { get; set; }

		[ConfigurableNumericValue(Name = "WeightOfTime", RangeFrom = 1, RangeTo = 1000)]
		public int WeightOfTime { get; set; }

		[ConfigurableNumericValue(Name = "WeightOfColour", RangeFrom = 1, RangeTo = 1000)]
		public int WeightOfColour { get; set; }

		[ConfigurableNumericValue(Name = "WeightConvexHullSize", RangeFrom = 1, RangeTo = 1000)]
		public int WeightConvexHullSize { get; set; }

		[ConfigurableNumericValue(Name = "MaxMatchingPoints", RangeFrom = 1, RangeTo = 100000)]
		public int MinMatchingPoints { get; set; }

		[ServiceRequest]
		public ITimeService TimeService { get; set; }

		[ServiceRequest]
		public IResetHandlerService ResetHandlerService {
			set { value.Reset += OnReset; }
		}

		#endregion

		#region Public Methods

		public MatchNearestClusterFirst() {
			MaxDistanceBetweenTwoCluster = 30;
			MaxFramesBetweenTwoCluster = 4;
			WeightOfDistance = 500;
			WeightOfTime = 500;
			WeightOfColour = 50;
			WeightConvexHullSize = 500;
			MinMatchingPoints = 100000;
			DoReset();
		}

		#endregion

		#region Private Methods

		protected override IList<Cluster> Convert(IList<IList<ColoredPoint>> toProcess) {
			if (toProcess != null) {
				// prepare data
				foreach (var pointList in toProcess) {
					m_availableClusters.Add(new Cluster(pointList, TimeService.GetTime()));
				}
			}
			IList<Cluster> toReturn = FindMovement();
			return toReturn;
		}

		/// <summary>
		/// Reset handler
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="args"></param>
		private void OnReset(Object sender, EventArgs args) {
			DoReset();
		}

		/// <summary>
		/// resets the step
		/// </summary>
		private void DoReset() {
			m_availableClusters.Clear();
			m_minDistance = double.MaxValue;
			m_minColor = double.MaxValue;
			m_minSize = double.MaxValue;
			m_minTime = double.MaxValue;
			m_maxColor = double.MinValue;
			m_maxDistance = double.MinValue;
			m_maxSize = double.MinValue;
			m_maxTime = double.MinValue;
		}

		/// <summary>
		/// Calculates how close two given clusters are
		/// </summary>
		/// <param name="first"></param>
		/// <param name="last"></param>
		/// <returns></returns>
		private double MatchFunction(Cluster first, Cluster last) {
			if (first == last) {
				return double.MinValue;
			}
			if (last.PreviousCluster != null) {
				return double.MinValue;
			}
			double matchingPoints = 0;
			double distance =
				Math.Pow(
					Math.Pow(first.AverageXValue - last.AverageXValue, 2) + Math.Pow(first.AverageYValue - last.AverageYValue, 2),
					0.5);
			if (MaxDistanceBetweenTwoCluster < distance) {
				return double.MinValue;
			}
			m_minDistance = Math.Min(m_minDistance, distance);
			m_maxDistance = Math.Max(m_maxDistance, distance);

			matchingPoints -= (distance / ReturnOneIfNull(m_maxDistance - m_minDistance) * WeightOfDistance);

			int timeDiff = Math.Abs(first.Time - last.Time);
			if (first.Time >= last.Time || timeDiff > MaxFramesBetweenTwoCluster) {
				return double.MinValue;
			}
			m_minTime = Math.Min(m_minTime, timeDiff);
			m_maxTime = Math.Max(m_maxTime, timeDiff);
			matchingPoints -= (timeDiff - 1) / ReturnOneIfNull(m_maxTime - m_minTime) * WeightOfTime;

			double colorDifferenceDifference =
				Math.Pow(
					Math.Pow(first.ColorY - last.ColorY, 2) + Math.Pow(first.ColorX - last.ColorX, 2) +
					Math.Pow(first.ColorZ - last.ColorZ, 2), 0.5);

			m_minColor = Math.Min(m_minColor, colorDifferenceDifference);
			m_maxColor = Math.Max(m_maxColor, colorDifferenceDifference);
			matchingPoints -= (colorDifferenceDifference) / ReturnOneIfNull(m_maxColor - m_minColor) * WeightOfColour;

			double diffConvexHullSize = Math.Abs(first.HulPerimeterSize - last.HulPerimeterSize);

			m_minSize = Math.Min(m_minSize, diffConvexHullSize);
			m_maxSize = Math.Max(m_maxSize, diffConvexHullSize);
			matchingPoints -= ((diffConvexHullSize) / ReturnOneIfNull(m_maxSize - m_minSize)) * WeightConvexHullSize;

			if (matchingPoints < (-1) * MinMatchingPoints) {
				matchingPoints = Double.MinValue;
			}
			return matchingPoints;
		}

		/// <summary>
		/// helper function to avoid null values
		/// </summary>
		/// <param name="d"></param>
		/// <returns></returns>
		private static double ReturnOneIfNull(double d) {
			if (d == 0) {
				return 1;
			}
			return d;
		}

		/// <summary>
		/// match clusters together
		/// </summary>
		/// <returns></returns>
		private IList<Cluster> FindMovement() {
			if (m_availableClusters.Count < 1) {
				return null;
			}
			// remove to old clusters
			int maxTime = MaxFramesBetweenTwoCluster;
			int time = TimeService.GetTime();
			m_availableClusters.RemoveAll(c => time - c.Time > maxTime);

			// rember found clusters
			IList<Cluster> toReturn = new List<Cluster>();
			IList<Cluster> toRemove = new List<Cluster>();
			Cluster firstCluster;
			Cluster lastCluster;

			var candidates = new List<Cluster>(m_availableClusters);
			var usedClusters = new List<Cluster>();
			foreach (Cluster cluster in m_availableClusters) {
				if (cluster == null) {
					continue;
				}
				if (usedClusters.Contains(cluster)) {
					continue;
				}
				firstCluster = cluster;
				var sortedCluster = candidates.Select(c => new { Match = MatchFunction(firstCluster, c), Cluster = c });
				var result = sortedCluster.OrderBy(c => c.Match).LastOrDefault();
				if (result.Match == Double.MinValue) {
					// no match found
				}
				else {
					lastCluster = result.Cluster;

					usedClusters.Add(lastCluster);
					usedClusters.Add(firstCluster);
					lastCluster.PreviousCluster = firstCluster;
					toReturn.Add(lastCluster);
					candidates.Remove(lastCluster);
					candidates.Remove(firstCluster);
					toRemove.Add(firstCluster);
				}
			}
			foreach (Cluster cluster in toRemove) {
				m_availableClusters.Remove(cluster);
			}
			StatisticValuesProviderService.SetStatisticValue("Movements.Found", toReturn.Count);
			return toReturn;
		}

		#endregion

		#region Private Fields

		private readonly List<Cluster> m_availableClusters = new List<Cluster>();
		private double m_maxColor;
		private double m_maxDistance;
		private double m_maxSize;
		private double m_maxTime;
		private double m_minColor;
		private double m_minDistance;
		private double m_minSize;
		private double m_minTime;

		#endregion
	}
}